package com.zhouheng.springbootmultithreading.module.task;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * 模拟预处理算法数据
 *
 * @author 周恒
 * @date 20191014 15:30:06
 * @since v1.0
 */
@Component
public class AlgorithmTask {

    /**
     * threadPoolTaskExecutor 多算法异步线程
     */
    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;

    public AlgorithmTask(ThreadPoolTaskExecutor threadPoolTaskExecutor) {
        this.threadPoolTaskExecutor = threadPoolTaskExecutor;
    }

    /**
     * 需要注意开启任务调度@EnableScheduling在启动类上
     *
     * 模拟异步执行算法，并算法执行完再进行评分
     *
     * 需要注意的地方
     * 1.最大线程数为threadPool.setMaxPoolSize(42)，而缓冲队列threadPool.setQueueCapacity(0)时，如果没有future时，线程一直增加超过42时就会报错，
     * 如果把QueueCapacity设置大一点，超过42时，会放到队列中，超过队列值才会报错
     * 2.线程池方法，algorithm1，algorithm2，algorithm3，执行报错会导致整个线程池down掉，所以要么在submit处捕获，要么直接在方法中就捕获，推荐后者
     * 3.future的使用，会在主线程中等待获取future.get()中的所有异步线程任务都执行完，才执行下一步
     * 4.如果算法中，需要对同一个入参对象进行修改，那么应该在该算法方法中先clone一个对象再进行操作
     * BeanUtils.copyProperties方法clone太多了的话，反射很好内存，可以直接写get/set代码克隆
     * （如果调用了feign中修改的不用怕，因为经过序列化和反序列化后都不是同一个对象了）
     * 5.如果不需要future时，异步算法又在循环中时，也要注意超圈后（前一次循环的算法没执行完，后面的循环又开始了），
     * 对共同入参对象的修改也会造成上一次循环的算法的脏读，所以出现超圈异步有入参修改时，在循环时也需要克隆了再传到异步方法中
     * 6.如果异步算法中需要对数据库相同的数据进行增删改操作，会出现问题，这里就需要添加同步
     * 但是不建议这么做，最好在业务层面就避免，比如设计上让每个异步线程不会对相同的数据库数据进行增删改的操作
     * 例如1：算法1，执行完成时，先删除该算法1的数据 再 保存，算法2、3同理，就不会造成脏读
     * 例如2：评分时，每种模式资料都要评分，但是评分是查出所有预报时段的数据，其余模式的也会被查出来，
     * 当多种模式对同一预报时段异步评分时，对评分数据的增删改就会导致数据的错误，
     * 我们就需要在评分的代码上，加一个模式的入参，这样应该能（没试过）避免误读
     *
     * @author 周恒
     * @date 20191014 15:41:04
     * @since v1.0
     */
    @Scheduled(cron = "0 50 15 14 10 ? ")
    public void preAlgorithm(){
        List<Future<Boolean>> futures = new ArrayList<>(3);
        //注意6 ，如果超圈修改共同入参对象，时也需要先克隆了再修改，再传参
        futures.add(threadPoolTaskExecutor.submit(this::algorithm1));
        futures.add(threadPoolTaskExecutor.submit(this::algorithm2));
        futures.add(threadPoolTaskExecutor.submit(this::algorithm3));
        futures.forEach(future->{
            try{
                //future，会获取线程返回结果，没有返回值的也可以，例如algorithm1、2、3都没有返回值
                //如果线程未执行完毕，会进入阻塞，知道获取到为止
                Boolean algorithmFlag = future.get();
                if (algorithmFlag) {
                    // TODO 后续操作，以及返回DTO等
                }
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });
        //异步线程能保证多种算法同步执行，而futures能保证多种算法都执行完，才进行下一步的操作，例如：评分
        algorithmScore();
    }

    private Boolean algorithm1(){
        // 模拟执行算法1
        // 需要try catch捕获异常，避免线程池奔溃
        // 注意4的情况需要先new一个cloneObj，然后克隆BeanUtils.copyProperties(oldObj, cloneObj);，再对自己的cloneObj进行修改
        return true;
    }

    private Boolean algorithm2(){
        // 模拟执行算法2
        // 需要try catch捕获异常，避免线程池奔溃
        // 注意4的情况需要先new一个cloneObj，然后克隆BeanUtils.copyProperties(oldObj, cloneObj);，再对自己的cloneObj进行修改
        return true;
    }

    private Boolean algorithm3(){
        //模拟执行算法3
        // 需要try catch捕获异常，避免线程池奔溃
        // 注意4的情况需要先new一个cloneObj，然后克隆BeanUtils.copyProperties(oldObj, cloneObj);，再对自己的cloneObj进行修改
        return true;
    }

    private int algorithmScore(){
        //模拟算法评分
        return 100;
    }

}
